package gtug.duesseldorf.server;

import gtug.duesseldorf.client.model.ClientCommand;
import gtug.duesseldorf.client.model.ClientLoginData;
import gtug.duesseldorf.client.model.ClientWaveId;
import gtug.duesseldorf.client.model.ClientWaveList;
import gtug.duesseldorf.client.res.Constants;

import java.io.IOException;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

import net.zschech.gwt.comet.server.CometSession;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.waveprotocol.wave.examples.fedone.waveclient.common.ClientBackend;
import org.waveprotocol.wave.examples.fedone.waveclient.common.ClientUtils;
import org.waveprotocol.wave.examples.fedone.waveclient.common.ClientWaveView;
import org.waveprotocol.wave.examples.fedone.waveclient.common.IndexEntry;
import org.waveprotocol.wave.examples.fedone.waveclient.common.WaveletOperationListener;
import org.waveprotocol.wave.examples.fedone.waveclient.console.ConsoleUtils;
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
import org.waveprotocol.wave.model.document.operation.DocInitializationCursor;
import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
import org.waveprotocol.wave.model.document.operation.impl.InitializationCursorAdapter;
import org.waveprotocol.wave.model.id.IdConstants;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.operation.wave.WaveletDocumentOperation;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.WaveletData;

import com.google.common.collect.Lists;
import com.google.inject.internal.ImmutableMap;

public class Client implements Serializable {

	private static final long serialVersionUID = 1L;

	private static final Log LOG = LogFactory.getLog(Client.class);

	/** All document rendering and operations are done to a single document. */
	private static final String MAIN_DOCUMENT_ID = "main";

	/**
	 * unique ClientId
	 */
	private final String m_clientId;

	/**
	 * Backend of ClientId
	 */
	private ClientBackend m_backend;

	/**
	 * currently open wave
	 */
	private ClientWaveView m_openWave = null;

	private CometSession m_clientConnection;

	/**
	 * called by {@link ClientFactory}
	 * 
	 * @param clientId
	 */
	protected Client(String clientId) {
		m_clientId = clientId;
	}

	/**
	 * init the client with LoginData, called by {@link ClientFactory}
	 * 
	 * @param loginData
	 * @param eventExecutorService
	 * @param domain
	 * @throws Exception
	 */
	protected void initClient(ClientLoginData loginData) throws Exception {

		try {
			String server = loginData.getServer();

			int port = Constants.WAVE_PORT;

			// extract port from server
			int pos = server.indexOf(':');
			if (pos != -1 && server.length() > pos) {
				try {
					port = Integer.parseInt(server.substring(pos + 1));
					server = server.substring(0, pos);
				} catch (Exception ignored) {

				}
			}

			m_backend = new ClientBackend(loginData.getUser(), server, port);
		} catch (IOException e) {
			LOG.error(ExceptionUtils.getStackTrace(e));
			throw new Exception(e.getLocalizedMessage());
		}

		// BufferedDocOpImpl waveData = (BufferedDocOpImpl)c.toArray()[0];
		// if (isConnected() == false) {
		// throwNotConnected();
		// }

		// List<IndexEntry> index =
		// ClientUtils.getIndexEntries(m_backend.getIndexWave());
		// addData("here");
		// for(int i = 0; i<index.size(); i++) {
		// //setOpenWave(m_backend.getWave(index.get(entry).getWaveId()));
		// addData("-> " + index.get(i).toString());
		// }

		m_backend.addWaveletOperationListener(new WaveletOperationListener() {

			@Override
			public void waveletDocumentUpdated(String author, WaveletData wavelet, WaveletDocumentOperation docOp) {

				StringBuffer buf = new StringBuffer();
				buf.append("waveDocUpdate");
				buf.append(" author: ").append(author);
				buf.append(" wavelet: ").append(wavelet);
				buf.append(" docOp: ").append(docOp);

				LOG.info(buf.toString());

				// addData(buf.toString());
				// addData("-> " + docOp);

				// addData("-> " + docOp.getOperation().getCharactersString(0));
			}

			@Override
			public void participantRemoved(String author, WaveletData wavelet, ParticipantId participantId) {

				StringBuffer buf = new StringBuffer();

				buf.append("participantRemoved");
				buf.append(" author: " + author);
				buf.append(" wavelet: " + wavelet);
				buf.append(" partID: " + participantId);

				LOG.info(buf.toString());

				ClientCommand cc = new ClientCommand(new ClientWaveId(wavelet.getWaveletName().waveId.getId()));
				cc.removeParticipant(participantId.getAddress());

				addData(cc);
			}

			@Override
			public void participantAdded(String author, WaveletData wavelet, ParticipantId participantId) {

				StringBuffer buf = new StringBuffer();
				buf.append("partAdd");
				buf.append(" author: " + author);
				buf.append(" wavelet: " + wavelet);
				buf.append(" partId: " + participantId);

				LOG.info(buf.toString());

				ClientCommand cc = new ClientCommand(new ClientWaveId(wavelet.getWaveletName().waveId.getId()));
				cc.addParticipant(participantId.getAddress());

				addData(cc);
			}

			@Override
			public void onDeltaSequenceStart(WaveletData wavelet) {

				StringBuffer buf = new StringBuffer();
				buf.append("onDeltaSeqStart");
				buf.append(" wavelet: " + wavelet);

				LOG.info(buf.toString());
			}

			@Override
			public void onDeltaSequenceEnd(WaveletData wavelet) {

				StringBuffer buf = new StringBuffer();
				buf.append("onDeltaSeqEnd");
				buf.append(" wavelet: " + wavelet);

				LOG.info(buf.toString());

				String waveId = wavelet.getWaveletName().waveId.getId();
				String name = "";
				String digest = ClientUtils.render(wavelet.getDocuments().values());

				ClientCommand cc = new ClientCommand(new ClientWaveId(waveId));
				cc.setWaveList(new ClientWaveList(new ClientWaveId(waveId, name, digest), false));

				if (m_openWave != null) {
					cc.setMessageData(getWaveLines(m_openWave), false);
				}

				addData(cc);

				// ClientWaveId clientOpenWave = null;
				// if (m_openWave != null) {
				// clientOpenWave = new
				// ClientWaveId(m_openWave.getWaveId().getId());
				// addData(new ClientCommand(clientOpenWave));
				// }
				//
				// addData(new ClientCommand(new ClientWaveList(getInbox(),
				// true)));
				//
			}

			@Override
			public void noOp(String author, WaveletData wavelet) {

				StringBuffer buf = new StringBuffer();
				buf.append("noOp");
				buf.append(" author: " + author);
				buf.append(" wavelet: " + wavelet);

				LOG.info(buf.toString());
			}
		});
	}

	public String getClientId() {
		return m_clientId;
	}

	/**
	 * disconnect from server and shutdown backend
	 */
	public void serverDisconnect() {
		if (m_backend == null) {
			return;
		}

		m_backend.shutdown();
		m_backend = null;
	}

	/**
	 * create a new wave
	 * 
	 * @throws Exception
	 */
	public void createNewWave() throws Exception {
		throwNotConnected();

		m_backend.createConversationWave();
	}

	/**
	 * open wave with {@link WaveId} or throw an Exception if this wave is not
	 * available
	 * 
	 * @param waveId
	 * @throws Exception
	 */
	public void openWave(String waveId) throws Exception {
		throwNotConnected();

		List<IndexEntry> index = ClientUtils.getIndexEntries(m_backend.getIndexWave());

		for (IndexEntry e : index) {
			if (waveId.equals(e.getWaveId().getId())) {
				setOpenWave(m_backend.getWave(e.getWaveId()));
				return;
			}
		}

		throw new Exception(MessageFormat.format("No wave with id={0} available", new Object[] { waveId }));
	}

	public void addParticipant(String waveId, String participantName) throws Exception {
		throwNotConnected();

		if (false == isWaveOpen() || m_openWave.getWaveId().getId().equals(waveId) == false) {
			openWave(waveId);
		}

		String domain = m_openWave.getWaveId().getDomain();
		m_openWave.getWavelet(new WaveletId(domain, IdConstants.CONVERSATION_ROOT_WAVELET)).addParticipant(new ParticipantId(participantName));

		ClientWaveId clientWaveId = new ClientWaveId(m_openWave.getWaveId().getId());
		ClientCommand cc = new ClientCommand(clientWaveId);
		cc.addParticipant(participantName);
		addData(cc);
	}

	public void sendAppendMutation(String waveId, String text) throws Exception {
		if (text.length() == 0) {
			throw new IllegalArgumentException("Cannot append an empty String");
		} else {

			if (false == isWaveOpen() || m_openWave.getWaveId().getId().equals(waveId) == false) {
				openWave(waveId);
			}

			BufferedDocOp openDoc = getOpenDocument();
			int docSize = (openDoc == null) ? 0 : ClientUtils.findDocumentSize(openDoc);
			DocOpBuilder docOp = new DocOpBuilder();

			if (docSize > 0) {
				docOp.retain(docSize);
			}

			docOp.elementStart(ConsoleUtils.LINE, new AttributesImpl(ImmutableMap
					.of(ConsoleUtils.LINE_AUTHOR, m_backend.getUserId().getAddress())));
			docOp.elementEnd();
			docOp.characters(text);

			m_backend.sendWaveletOperation(getOpenWavelet().getWaveletName(), new WaveletDocumentOperation(MAIN_DOCUMENT_ID, docOp.build()));
		}
	}

	private synchronized void addData(ClientCommand data) {
		LOG.info("addData: " + data.toString());

		if (m_clientConnection != null) {
			m_clientConnection.enqueue(data);
		}
	}

	/**
	 * Set a wave as the open wave.
	 * 
	 * @param wave
	 *          to set as open
	 */
	private void setOpenWave(ClientWaveView wave) {
		if (ClientUtils.getConversationRoot(wave) == null) {
			wave.createWavelet(ClientUtils.getConversationRootId(wave));
		}

		m_openWave = wave;

		ClientWaveId clientWaveId = new ClientWaveId(m_openWave.getWaveId().getId());
		ClientCommand cc = new ClientCommand(clientWaveId);
		cc.setDoOpenWave(true);

		for (Object element : m_openWave.getWavelets()) {

			for (ParticipantId participantId : ((WaveletData) element).getParticipants()) {

				// TODO this is not complety true, because we take participants
				// of a
				// wavelet and at them all to one wave without considering the
				// individual wavelet

				cc.addParticipant(participantId.getAddress());

			}
		}
		cc.setMessageData(getWaveLines(m_openWave), false);

		addData(cc);
	}

	/**
	 * if user is not connected to a server throw {@link Exception}
	 * 
	 * @throws Exception
	 * 
	 * @throws Exception
	 */
	private void throwNotConnected() throws Exception {
		if (isConnected() == false) {
			throw new Exception("Error: not connected, run \"/connect user@domain server port\"");
		}
	}

	/**
	 * @return whether the client is connected to any server
	 */
	private boolean isConnected() {
		return m_backend != null;
	}

	/**
	 * @return the open wavelet of the open wave, or null if no wave is open
	 */
	private WaveletData getOpenWavelet() {
		return (m_openWave == null) ? null : ClientUtils.getConversationRoot(m_openWave);
	}

	/**
	 * @return open document, or null if no wave is open or main document doesn't
	 *         exist
	 */
	private BufferedDocOp getOpenDocument() {
		return getOpenWavelet() == null ? null : getOpenWavelet().getDocuments().get(MAIN_DOCUMENT_ID);
	}

	/**
	 * @return whether the client has a wave open
	 */
	private boolean isWaveOpen() {
		return isConnected() && m_openWave != null;
	}

	/**
	 * get inbox-entries
	 * 
	 * @return
	 */
	private ArrayList<ClientWaveId> getInbox() {
		List<IndexEntry> indexEntries = ClientUtils.getIndexEntries(m_backend.getIndexWave());
		ArrayList<ClientWaveId> lines = Lists.newArrayList();

		for (int i = 0; i < indexEntries.size(); i++) {
			ClientWaveView wave = m_backend.getWave(indexEntries.get(i).getWaveId());

			ClientWaveId clientWaveId = new ClientWaveId(wave.getWaveId().getId(), String.format("%4d) ", i), ConsoleUtils
					.renderNice(indexEntries.get(i).getDigest()));

			lines.add(clientWaveId);

			// StringBuilder line = new StringBuilder();
			// if ((wave == null)
			// || (ClientUtils.getConversationRoot(wave) == null)) {
			// line.append("...");
			// } else {
			// // HashedVersion version = wave.getWaveletVersion(ClientUtils
			// // .getConversationRootId(wave));
			//
			// line.append(String.format("%4d) ", i));
			// line.append(String.format("(%s) ", wave.getWaveId().getId()));
			// line.append(ConsoleUtils.renderNice(indexEntries.get(i)
			// .getDigest()));
			//
			// // GSE: add this later to inform user about updates waves
			//
			// // if (wave == openWave) {
			// // ansiCodes.add(ConsoleUtils.ANSI_BLUE_BG);
			// // ansiCodes.add(ConsoleUtils.ANSI_WHITE_FG);
			// // lastSeenVersions.put(wave, version);
			// // } else if (!version.equals(lastSeenVersions.get(wave))) {
			// // ansiCodes.add(ConsoleUtils.ANSI_BOLD);
			// // }
			// }
		}

		return lines;
	}

	private List<String> getWaveLines(ClientWaveView waveView) {
		final List<String> lines = Lists.newArrayList();
		final StringBuilder currentLine = new StringBuilder();
		final Deque<String> elemStack = new LinkedList<String>();

		for (BufferedDocOp document : ClientUtils.getConversationRoot(waveView).getDocuments().values()) {

			document.apply(new InitializationCursorAdapter(new DocInitializationCursor() {
				@Override
				public void characters(String s) {
					currentLine.append(ConsoleUtils.renderNice(s));
				}

				@Override
				public void elementStart(String type, Attributes attrs) {
					elemStack.push(type);

					if (type.equals(ConsoleUtils.LINE)) {
						if (!attrs.containsKey(ConsoleUtils.LINE_AUTHOR)) {
							throw new IllegalArgumentException("Line element must have author");
						}

						if (currentLine.length() != 0) {
							lines.add(currentLine.toString());
							currentLine.setLength(0);
						}

						lines.add(" ");

						lines.add(attrs.get(ConsoleUtils.LINE_AUTHOR));

					} else {
						throw new IllegalArgumentException("Unsupported element type " + type);
					}
				}

				@Override
				public void elementEnd() {
				}

				@Override
				public void annotationBoundary(AnnotationBoundaryMap map) {
				}
			}));
		}

		lines.add(currentLine.toString());

		return lines;
	}

	public void registerConnection(CometSession cometSession) {
		m_clientConnection = cometSession;
	}

	// @Override
	// public String[] waveListing() {
	//
	// // get wave list
	// List<IndexEntry> index = ClientUtils.getIndexEntries(m_backend
	// .getIndexWave());
	//
	// // set size
	// String[] listing = new String[index.size()];
	//
	// for (int i = 0; i < index.size(); i++) {
	// listing[i] = index.get(i).getWaveId().toString();
	// // listing[i] = listing[i].replace("[WaveId:", "");
	// // listing[i] = listing[i].replace("]", "");
	// }
	//
	// return listing;
	// }
}
